# 第04章 Configuration子节点解析

XMLConfigBuilder调用parse()方法:会从XPathParser中取出 <configuration>节点对应的Node对象,然后解析此Node节点的子Node:properties, settings, typeAliases,typeHandlers, objectFactory, objectWrapperFactory, plugins, environments,databaseIdProvider, mappers

 public Configuration parse() {
     if (parsed) {
         throw new BuilderException("Each XMLConfigBuilder can only be used once.");
     }
     parsed = true;
     parseConfiguration(parser.evalNode("/configuration"));
     return configuration;
 }

/**
 * 解析 "/configuration"节点下的子节点信息,然后将解析的结果设置到Configuration对象中
 */
private void parseConfiguration(XNode root) {
    try {
        //1.首先处理properties 节点
        propertiesElement(root.evalNode("properties"));
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        loadCustomVfs(settings);
        loadCustomLogImpl(settings);
        //2.处理typeAliases
        typeAliasesElement(root.evalNode("typeAliases"));
        //3.处理插件
        pluginElement(root.evalNode("plugins"));
        //4.处理objectFactory
        objectFactoryElement(root.evalNode("objectFactory"));
        //5.objectWrapperFactory
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        //6.settings
        settingsElement(settings);
        //7.处理environments
        environmentsElement(root.evalNode("environments"));
        //8.databaseIdProvider
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        //9.typeHandlers
        typeHandlerElement(root.evalNode("typeHandlers"));
        //10.mappers
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

# 4.1 XNode节点获取

parser.evalNode("/configuration")
1

XPathParser中evalNode方法如下:

public XNode evalNode(String expression) {
    return evalNode(document, expression);
}

public XNode evalNode(Object root, String expression) {
    Node node = (Node) evaluate(expression, root, XPathConstants.NODE);
    if (node == null) {
        return null;
    }
    return new XNode(this, node, variables);
}

private Object evaluate(String expression, Object root, QName returnType) {
    try {
        return xpath.evaluate(expression, root, returnType);
    } catch (Exception e) {
        throw new BuilderException("Error evaluating XPath.  Cause: " + e, e);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

即解析root节点为configuration的内容。

# 4.2 解析<properties>节点

mybatis-config.xml中<properties>节点的定义如下:

<properties resource="db.properties">
    <!--<property name="username" value="dev_user"/>-->
    <!--<property name="password" value="F2Fa3!33TYyg"/>-->
</properties>
1
2
3
4

解析过程:

propertiesElement(root.evalNode("properties"));
1

其中,root.evalNode("properties")和4.1一样,会解析节点为properties的内容,propertiesElement(...)方法如下:

/**
* @Param context <properties>节点
*/
private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
         // 获取<properties>节点的所有子节点
        Properties defaults = context.getChildrenAsProperties();
        // 获取<properties>节点上的resource属性
        String resource = context.getStringAttribute("resource");
        // 获取<properties>节点上的url属性
        String url = context.getStringAttribute("url");
        // resource和url不能同时存在
        if (resource != null && url != null) {
            throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
        }
        if (resource != null) {
            // 获取resource属性值对应的properties文件中的键值对,并添加至defaults容器中 
            defaults.putAll(Resources.getResourceAsProperties(resource));
        } else if (url != null) {
            // 获取url属性值对应的properties文件中的键值对,并添加至defaults容器中
            defaults.putAll(Resources.getUrlAsProperties(url));
        }
        // 获取configuration中原本的属性,并添加至defaults容器中
        Properties vars = configuration.getVariables();
        if (vars != null) {
            defaults.putAll(vars);
        }
        parser.setVariables(defaults);
        // 将defaults容器添加至configuration中
        configuration.setVariables(defaults);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
  • 首先读取<resources>节点下的所有<resource>节点,并将每个节点的namevalue属性存入Properties中。
  • 然后读取<resources>节点上的resourceurl属性,并获取指定配置文件中的namevalue,也存入Properties中。(PS:由此可知,如果resource节点上定义的属性和properties文件中的属性重名,那么properties文件中的属性值会覆盖resource节点上定义的属性值。)
  • 最终,携带所有属性的Properties对象会被存储在Configuration对象中。

# 4.3 解析<setting>节点

节点定义:

<settings>
  <setting name="cacheEnabled" value="true"/>
  <setting name="lazyLoadingEnabled" value="true"/>
  <setting name="multipleResultSetsEnabled" value="true"/>
</settings>
1
2
3
4
5

解析代码:

Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
settingsElement(settings);
1
2
3
4

首先看settingsAsProperties(root.evalNode("settings")):

// setting as properties
private Properties settingsAsProperties(XNode context) {
    if (context == null) {
      // 如果没有配置<settings>节点返回一个空的Properties
      return new Properties();
    }
    // 获取<setting>节点的所有子节点
    Properties props = context.getChildrenAsProperties();
    // 检查所有的settings是由configuration对象所知晓的
    // 创建MetaClass
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    // 遍历子节点props
    for (Object key : props.keySet()) {
        // 如果不含该项设置,则报异常
      if (!metaConfig.hasSetter(String.valueOf(key))) {
        throw new BuilderException("The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
      }
    }
    return props;
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

Properties是Hashtable的子类,即是key&value的形式,settings里面的数据被解析成Properties,之后还有一步,<settings>标签下的每个<setting>中的name属性不是随便填写的,都需是MyBatis支持的配置,因此需要对Properties里面的Key做一个校验,如果有一个name不是MyBatis支持的就会抛出异常,MyBatis初始化整体失败。

至于具体校验的是哪些Key,这就要跟一下具体代码,首先是MetaClass.forClass(Configuration.class, localReflectorFactory),第二个实参是XMLConfigBuilder里面直接new出来的,它的实际类型为DefaultReflectorFactory,看一下forClass方法实现:

public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
    return new MetaClass(type, reflectorFactory);
}
1
2
3

而new MetaClass做了如下操作:

private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
    this.reflectorFactory = reflectorFactory;
    this.reflector = reflectorFactory.findForClass(type);
}
1
2
3
4

显而易见,继续跟一下第3行的代码DefaultRelectorFactory的findForClass方法:

public Reflector findForClass(Class<?> type) {
    if (classCacheEnabled) {
        // synchronized (type) removed see issue #461
        return reflectorMap.computeIfAbsent(type, Reflector::new);
    } else {
        return new Reflector(type);
    }
}
1
2
3
4
5
6
7
8

不管怎么样都会执行new Reflector(type)这一句代码,看一下此时做了什么事,注意传入的参数是Configuration的class对象:

public Reflector(Class<?> clazz) {
    type = clazz;
    addDefaultConstructor(clazz);
    addGetMethods(clazz);
    addSetMethods(clazz);
    addFields(clazz);
    readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);
    writeablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);
    for (String propName : readablePropertyNames) {
        caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
    }
    for (String propName : writeablePropertyNames) {
        caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这么多方法至于具体要看哪个,要注意的是之前XMLConfigBuilder里面对于Key的判断是"!metaConfig.hasSetter(String.valueOf(key))",代码的意思是判断的是否Key有set方法,那么显而易见这里要继续跟第5行的addSetMethods方法:

private void addGetMethods(Class<?> cls) {
    Map<String, List<Method>> conflictingGetters = new HashMap<>();
    // 获取Configuration类的类方法
    Method[] methods = getClassMethods(cls);
    // 遍历Configuration类的类方法
    for (Method method : methods) {
        if (method.getParameterTypes().length > 0) {
            continue;
        }
        // 获取方法名称
        String name = method.getName();
        if ((name.startsWith("get") && name.length() > 3)
            || (name.startsWith("is") && name.length() > 2)) {
            // 如果是setter方法,添加
            name = PropertyNamer.methodToProperty(name);
            addMethodConflict(conflictingGetters, name, method);
        }
    }
    resolveGetterConflicts(conflictingGetters);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

到这里应该很明显了,结论就是:<setting>的name属性对应的值,必须在Configuration类有相应的Setter,比如设置了一个属性useGenerateKeys方法,那么必须在Configuration类中有setUseGenerateKeys方法才行

顺便说一下,resolveSetterConflicts方法,其作用是:Setter有可能在类中被重载导致有多个,此时取Setter中方法参数只有一个且参数类型与Getter一致的Setter

# 4.4 解析<typeAliases>节点

<typeAliases>属性的定义方式有如下两种:

方式一:

<typeAliases>
  <typeAlias alias="Author" type="domain.blog.Author"/>
  <typeAlias alias="Blog" type="domain.blog.Blog"/>
</typeAliases>
1
2
3
4

方式二:

<typeAliases>
  <package name="domain.blog"/>
</typeAliases>
1
2
3

采用这种方式时,MyBatis会为指定包下的所有类起一个别名,该别名为首字母小写的类名。

<typeAliases>节点的解析过程如下:

typeAliasesElement(root.evalNode("typeAliases"));

// <typeAliases>节点的解析
private void typeAliasesElement(XNode parent) {
    if (parent != null) {
      // 遍历<typeAliases>下的所有子节点
      for (XNode child : parent.getChildren()) {
        // 方式二
        if ("package".equals(child.getName())) {
          // 获取<package>上的name属性(包名)
          String typeAliasPackage = child.getStringAttribute("name");
          // 为该包下的所有类起个别名,并注册进configuration的typeAliasRegistry中 
          configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
        } else {
          // 方式一
           // 获取alias和type属性
          String alias = child.getStringAttribute("alias");
          String type = child.getStringAttribute("type");
          // 注册进configuration的typeAliasRegistry中
          try {
            Class<?> clazz = Resources.classForName(type);
            if (alias == null) {
              typeAliasRegistry.registerAlias(clazz);
            } else {
              typeAliasRegistry.registerAlias(alias, clazz);
            }
          } catch (ClassNotFoundException e) {
            throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
          }
        }
      }
    }
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
  • 如果<typeAliases>节点下定义了<package>节点,那么MyBatis会给该包下的所有类起一个别名(以类名首字母小写作为别名)
  • 如果<typeAliases>节点下定义了<typeAlias>节点,那么MyBatis就会给指定的类起指定的别名。
  • 这些别名都会被存入configurationtypeAliasRegistry容器中。

# 4.5 解析<environments>节点

节点定义:

<environments default="development">
    <environment id="development">
        <transactionManager type="JDBC"/>
        <dataSource type="POOLED">
            <property name="driver" value="${driver}"/>
            <property name="url" value="${url}"/>
            <property name="username" value="${username}"/>
            <property name="password" value="${password}"/>
        </dataSource>
    </environment>
</environments>
1
2
3
4
5
6
7
8
9
10
11

解析方法调用:

environmentsElement(root.evalNode("environments"));
1

具体解析:

/**
 * 解析environments节点,并将结果设置到Configuration对象中
 * 注意:创建envronment时,如果SqlSessionFactoryBuilder指定了特定的环境(即数据源)
 * 则返回指定环境(数据源)的Environment对象,否则返回默认的Environment对象;
 * 这种种方式实现了MyBatis可以连接多数据源
*/
private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        if (environment == null) {
            environment = context.getStringAttribute("default");
        }
        for (XNode child : context.getChildren()) {
            // 获取子节点的id属性
            String id = child.getStringAttribute("id");
            if (isSpecifiedEnvironment(id)) {
                //1.创建事务工厂 TransactionFactory
                TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
                //2.创建数据源DataSource工厂
                DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
                //3.创建数据源DataSource
                DataSource dataSource = dsFactory.getDataSource();
                //4.构造Environment对象
                Environment.Builder environmentBuilder = new Environment.Builder(id)
                    .transactionFactory(txFactory)
                    .dataSource(dataSource);
                //5.将创建的Envronment对象设置到configuration 对象中
                configuration.setEnvironment(environmentBuilder.build());
            }
        }
    }
}

private boolean isSpecifiedEnvironment(String id) {
    if (environment == null) {
        throw new BuilderException("No environment specified.");
    } else if (id == null) {
        throw new BuilderException("Environment requires an id attribute.");
    } else if (environment.equals(id)) {
        return true;
    }
    return false;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

其中,事务工厂的创建:

//1.创建事务工厂 TransactionFactory
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
1
2

配置如下:

<transactionManager type="JDBC"/>
1

Configuration中别名对应:

typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
1

即事务工厂为JdbcTransactionFactory,另外的事务工厂还有ManagedTransactionFactory

关于事务的细节,将在后面的章节解析。

数据源工厂和数据源创建:

//2.创建数据源DataSource工厂
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
//3.创建数据源DataSource
DataSource dataSource = dsFactory.getDataSource();
1
2
3
4

配置如下:

<dataSource type="POOLED">
    <property name="driver" value="${driver}"/>
    <property name="url" value="${url}"/>
    <property name="username" value="${username}"/>
    <property name="password" value="${password}"/>
</dataSource>
1
2
3
4
5
6

Configuration中别名对应:

typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
1

即数据源工厂为PooledDataSourceFactory,另外的数据源工厂还包括JndiDataSourceFactory,UnpooledDataSourceFactory

public DataSource getDataSource() {
    return dataSource;
}
1
2
3

关于数据源及连接池的具体细节,将在下一章进行阐述。

# 4.6 解析<databaseIdProvider>节点

databaseIdProvider数据库厂商标识,一般在形同数据库厂商的环境下,数据库厂商标识没有什么意义,在实际的应用中使用得较少,因为使用不同厂商数据库的系统还是比较少的。Mybatis可能会运行在不同厂商的数据库中,它为此提供了一个数据库标识,并提供自定义,它的作用在于指定SQL到对应的数据库厂商提供的数据库中运行。

配置如下:

<databaseIdProvider type="DB_VENDOR">
  <property name="MySQL" value="mysql"/>       
  <property name="Oracle" value="oracle" />
</databaseIdProvider>
1
2
3
4

或者在Mapper.xml文件中SQL中设置databaseId:

<select id="getOtaPackage" 
        parameterType="string" 
        resultType="otaPackage" 
        databaseId="mysql">
    select 
    package_name as packageName, package_size as packageSize 
    from ota_package 
    where 
    package_id = #{packageId};
</select>
1
2
3
4
5
6
7
8
9
10

XMLConfigBuilder中调用解析语句:

databaseIdProviderElement(root.evalNode("databaseIdProvider"));
1

解析数据库厂商标识:

private void databaseIdProviderElement(XNode context) throws Exception {
    DatabaseIdProvider databaseIdProvider = null;
    // 如果XML中配置了databaseIdProvider标签的节点
    if (context != null) {
        // 获取<databaseIdProvider>节点的type属性
        String type = context.getStringAttribute("type");
        // 如果type是VENDOR则重新设置为DB_VENDOR
        if ("VENDOR".equals(type)) {
            type = "DB_VENDOR";
        }
        // 获取<databaseIdProvider>的所有的子节点
        Properties properties = context.getChildrenAsProperties();
        databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
        // 设置子节点属性到DatabaseIdProvider对象中
        databaseIdProvider.setProperties(properties);
    }
    // 根据配置对象获取环境变量
    Environment environment = configuration.getEnvironment();
    if (environment != null && databaseIdProvider != null) {
        // 环境变量和厂商标识均为null,则使用databaseId
        String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
        // 往配置对象中设置databaseId
        configuration.setDatabaseId(databaseId);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

对13~14行代码解析如下:

BaseBuilder.java的别名解析类的方法:

protected <T> Class<? extends T> resolveClass(String alias) {
    if (alias == null) {
        return null;
    }
    try {
        return resolveAlias(alias);
    } catch (Exception e) {
        throw new BuilderException("Error resolving class. Cause: " + e, e);
    }
}

// 从别名注册器的容器中获取别名对应的类
protected <T> Class<? extends T> resolveAlias(String alias) {
    return typeAliasRegistry.resolveAlias(alias);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

前面在new Configuration()的时候已经创建了别名为DB_VENDOR的别名和值:

public Configuration() {
    // ...
    typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
    //...
}
1
2
3
4
5

其中TypeAliasRegistry的resolveAlias方法如下:

public <T> Class<T> resolveAlias(String string) {
    try {
        if (string == null) {
            return null;
        }
        // 国际化
        String key = string.toLowerCase(Locale.ENGLISH);
        Class<T> value;
        if (TYPE_ALIASES.containsKey(key)) {
            value = (Class<T>) TYPE_ALIASES.get(key);
        } else {
            value = (Class<T>) Resources.classForName(string);
        }
        return value;
    } catch (ClassNotFoundException e) {
        throw new TypeException("Could not resolve type alias '" + string + "'.  Cause: " + e, e);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

然后,根据返回了Class类型,调用newInstance方法创建DatabaseIdProvider对象,并设置配置:

databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
databaseIdProvider.setProperties(properties);
1
2

# 4.7 解析<typeHandlers>节点

无论是 MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,还是从结果集中取出一个值时,都会用类型处理器将获取的值以合适的方式转换成 Java 类型。Mybatis默认为我们实现了许多TypeHandler, 当我们没有配置指定TypeHandler时,Mybatis会根据参数或者返回结果的不同,默认为我们选择合适的TypeHandler处理。

配置TypeHandler:

<configuration>
    <typeHandlers>
      <!-- 
          当配置package的时候,mybatis会去配置的package扫描TypeHandler
          <package name="com.dy.demo"/>
       -->
      
      <!-- handler属性直接配置我们要指定的TypeHandler -->
      <typeHandler handler=""/>
      
      <!-- javaType 配置java类型,例如String, 如果配上javaType, 
		那么指定的typeHandler就只作用于指定的类型 -->
      <typeHandler javaType="" handler=""/>
      
      <!-- jdbcType 配置数据库基本数据类型,例如varchar, 
		如果配上jdbcType, 那么指定的typeHandler就只作用于指定的类型  -->
      <typeHandler jdbcType="" handler=""/>
      
      <!-- 也可两者都配置 -->
      <typeHandler javaType="" jdbcType="" handler=""/>
      
  </typeHandlers>
  
  ......
  
</configuration>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

调用:

typeHandlerElement(root.evalNode("typeHandlers"));
1

解析:

/**
 * 解析typeHandlers节点
 */
private void typeHandlerElement(XNode parent) {
    if (parent != null) {
        for (XNode child : parent.getChildren()) {
            //子节点为package时,获取其name属性的值,然后自动扫描package下的自定义typeHandler
            if ("package".equals(child.getName())) {
                String typeHandlerPackage = child.getStringAttribute("name");
                typeHandlerRegistry.register(typeHandlerPackage);
            } else {
          //子节点为typeHandler时, 可以指定javaType属性, 也可以指定jdbcType, 也可两者都指定
          //javaType 是指定java类型
          //jdbcType 是指定jdbc类型(数据库类型: 如varchar)
                String javaTypeName = child.getStringAttribute("javaType");
                String jdbcTypeName = child.getStringAttribute("jdbcType");
                //handler就是我们配置的typeHandler
                String handlerTypeName = child.getStringAttribute("handler");
                //resolveClass方法就是前面所讲的TypeAliasRegistry里面处理别名的方法
                Class<?> javaTypeClass = resolveClass(javaTypeName);
                //JdbcType是一个枚举类型,resolveJdbcType方法是在获取枚举类型的值
                JdbcType jdbcType = resolveJdbcType(jdbcTypeName);
                Class<?> typeHandlerClass = resolveClass(handlerTypeName);
                //注册typeHandler, typeHandler通过TypeHandlerRegistry这个类管理
                if (javaTypeClass != null) {
                    if (jdbcType == null) {
                        typeHandlerRegistry.register(javaTypeClass,
                                                     typeHandlerClass);
                    } else {
                        typeHandlerRegistry.register(javaTypeClass, 
                                                     jdbcType, typeHandlerClass);
                    }
                } else {
                    typeHandlerRegistry.register(typeHandlerClass);
                }
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

其中注册类型处理器的最终方法如下:

private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
    if (javaType != null) {
        Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType);
        if (map == null || map == NULL_TYPE_HANDLER_MAP) {
            map = new HashMap<>();
            TYPE_HANDLER_MAP.put(javaType, map);
        }
        map.put(jdbcType, handler);
    }
    ALL_TYPE_HANDLERS_MAP.put(handler.getClass(), handler);
}
1
2
3
4
5
6
7
8
9
10
11

# 4.8 解析<mappers>节点

<mappers>节点的定义方式有如下四种:

方式一:

<mappers>
  <package name="org.mybatis.builder"/>
</mappers>
1
2
3

方式二:

<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
</mappers>
1
2
3

方式三:

<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
</mappers>
1
2
3

方式四:

<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>
1
2
3

<mappers>节点的解析过程如下:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
        // 遍历<mappers>下所有子节点
        for (XNode child : parent.getChildren()) {
            // 如果当前节点为<package>
            if ("package".equals(child.getName())) {
                // 获取<package>的name属性(该属性值为mapper class所在的包名)
                String mapperPackage = child.getStringAttribute("name");
                // 将该包下的所有Mapper Class注册到configuration的mapperRegistry容器中
                configuration.addMappers(mapperPackage);
            // 如果当前节点为<mapper>
            } else {
                // 依次获取resource、url、class属性
                String resource = child.getStringAttribute("resource");
                String url = child.getStringAttribute("url");
                String mapperClass = child.getStringAttribute("class");
                // 解析resource属性(Mapper.xml文件的路径)
                if (resource != null && url == null && mapperClass == null) {
                    ErrorContext.instance().resource(resource);
                    // 将Mapper.xml文件解析成输入流
                    InputStream inputStream = Resources.
                        getResourceAsStream(resource);
                    // 使用XMLMapperBuilder解析Mapper.xml,并将Mapper Class
                    // 注册进configuration对象的mapperRegistry容器中
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(
                        inputStream, 
                        configuration, 
                        resource, 
                        configuration.getSqlFragments());
                    mapperParser.parse();
                // 解析url属性(Mapper.xml文件的路径)
                } else if (resource == null && url != null && mapperClass == null) {
                    ErrorContext.instance().resource(url);
                    InputStream inputStream = Resources.getUrlAsStream(url);
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(
                        inputStream, 
                        configuration, 
                        url, 
                        configuration.getSqlFragments());
                    mapperParser.parse();
                // 解析class属性(Mapper Class的全限定名)
                } else if (resource == null && url == null && mapperClass != null) {
                    // 将Mapper Class的权限定名转化成Class对象
                    Class<?> mapperInterface = Resources.classForName(mapperClass);
                    // 注册进configuration对象的mapperRegistry容器中
                    configuration.addMapper(mapperInterface);
                } else {
                    throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
                }
            }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
  • MyBatis会遍历<mappers>下所有的子节点,如果当前遍历到的节点是<package>,则MyBatis会将该包下的所有Mapper Class注册到configurationmapperRegistry容器中。
  • 如果当前节点为<mapper>,则会依次获取resource、url、class属性,解析映射文件,并将映射文件对应的Mapper Class注册到configurationmapperRegistry容器中。

其中,<mapper>节点的解析过程如下:

XMLMapperBuilder mapperParser = new XMLMapperBuilder(
    inputStream, 
    configuration, 
    resource, 
    configuration.getSqlFragments());
mapperParser.parse();
1
2
3
4
5
6

在解析前,首先需要创建XMLMapperBuilder,创建过程如下:

public XMLMapperBuilder(InputStream inputStream, 
                        Configuration configuration, 
                        String resource, 
                        Map<String, XNode> sqlFragments) {
    this(new XPathParser(inputStream, 
                         true, 
                         configuration.getVariables(), 
                         new XMLMapperEntityResolver()),
         configuration, 
         resource, 
         sqlFragments);
}

private XMLMapperBuilder(XPathParser parser, 
                         Configuration configuration, 
                         String resource, 
                         Map<String, XNode> sqlFragments) {
  // 将configuration赋给BaseBuilder
  super(configuration);
  // 创建MapperBuilderAssistant对象(该对象为MapperBuilder的协助者)
  this.builderAssistant = new  MapperBuilderAssistant(configuration, resource);
  this.parser = parser;
  this.sqlFragments = sqlFragments;
  this.resource = resource;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  • 首先会初始化父类BaseBuilder,并将configuration赋给BaseBuilder;
  • 然后创建MapperBuilderAssistant对象,该对象为XMLMapperBuilder的协助者,用来协助XMLMapperBuilder完成一些解析映射文件的动作。

当有了XMLMapperBuilder后,便可进入解析<mapper>的过程:

public void parse() {
  // 若当前的Mapper.xml尚未被解析,则开始解析
  // PS:若<mappers>节点下有相同的<mapper>节点,那么就无需再次解析了
  if (!configuration.isResourceLoaded(resource)) {
    // 解析<mapper>节点
    configurationElement(parser.evalNode("/mapper"));
    // 将该Mapper.xml添加至configuration的LoadedResource容器中,下回无需再解析
    configuration.addLoadedResource(resource);
    // 将该Mapper.xml对应的Mapper Class注册进configuration的mapperRegistry容器中
    bindMapperForNamespace();
  }

  parsePendingResultMaps();
  parsePendingCacheRefs();
  parsePendingStatements();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

Configuration的isResourceLoaded(...)方法如下:

public boolean isResourceLoaded(String resource) {
    return loadedResources.contains(resource);
}
1
2
3

其中,resource为Mapper.xml文件的路径

Configuration的addLoadedResource(...)方法如下:

public void addLoadedResource(String resource) {
    loadedResources.add(resource);
}
1
2
3

XMLMapperBuilder中的configurationElement函数

private void configurationElement(XNode context) {
try {
  // 获取<mapper>节点上的namespace属性,该属性必须存在,表示当前映射文件对应的Mapper Class是谁
  String namespace = context.getStringAttribute("namespace");
  if (namespace == null || namespace.equals("")) {
    throw new BuilderException("Mapper's namespace cannot be empty");
  }
  // 将namespace属性值赋给builderAssistant
  builderAssistant.setCurrentNamespace(namespace);
  // 解析<cache-ref>节点
  cacheRefElement(context.evalNode("cache-ref"));
  // 解析<cache>节点
  cacheElement(context.evalNode("cache"));
  // 解析<parameterMap>节点
  parameterMapElement(context.evalNodes("/mapper/parameterMap"));
  // 解析<resultMap>节点
  resultMapElements(context.evalNodes("/mapper/resultMap"));
  // 解析<sql>节点
  sqlElement(context.evalNodes("/mapper/sql"));
  // 解析sql语句      
  buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
  throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  • resultMapElements函数 该函数用于解析映射文件中所有的<resultMap>节点,这些节点会被解析成ResultMap对象,存储在Configuration对象的resultMaps容器中。

  • <resultMap>节点定义如下:

<resultMap id="userResultMap" type="User">
  <constructor>
     <idArg column="id" javaType="int"/>
     <arg column="username" javaType="String"/>
  </constructor>
  <result property="username" column="user_name"/>
  <result property="password" column="hashed_password"/>
</resultMap>
1
2
3
4
5
6
7
8
  • <resultMap>节点的解析过程:
private ResultMap resultMapElement(XNode resultMapNode, 
                                   List<ResultMapping> additionalResultMappings) throws Exception {
  ErrorContext.instance().activity("processing " + 
                                   resultMapNode.getValueBasedIdentifier());
  // 获取<ResultMap>上的id属性
  String id = resultMapNode.getStringAttribute("id",
                                           resultMapNode.getValueBasedIdentifier());
  // 获取<ResultMap>上的type属性(即resultMap的返回值类型)
  String type = resultMapNode.getStringAttribute("type",
    resultMapNode.getStringAttribute("ofType",
        resultMapNode.getStringAttribute("resultType",
            resultMapNode.getStringAttribute("javaType"))));
  // 获取extends属性
  String extend = resultMapNode.getStringAttribute("extends");
  // 获取autoMapping属性
  Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
  // 将resultMap的返回值类型转换成Class对象
  Class<?> typeClass = resolveClass(type);
  Discriminator discriminator = null;
  // resultMappings用于存储<resultMap>下所有的子节点
  List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
  resultMappings.addAll(additionalResultMappings);
  // 获取并遍历<resultMap>下所有的子节点
  List<XNode> resultChildren = resultMapNode.getChildren();
  for (XNode resultChild : resultChildren) {
    // 若当前节点为<constructor>,则将它的子节点们添加到resultMappings中去
    if ("constructor".equals(resultChild.getName())) {
      processConstructorElement(resultChild, typeClass, resultMappings);
    }
    // 若当前节点为<discriminator>,则进行条件判断,并将命中的子节点添加到resultMappings中去
    else if ("discriminator".equals(resultChild.getName())) {
      discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
    }
    // 若当前节点为<result>、<association>、<collection>,则将其添加到resultMappings中去
    else {
      // PS:flags仅用于区分当前节点是否是<id>或<idArg>,因为这两个节点的属性名为name,而其他节点的属性名为property
      List<ResultFlag> flags = new ArrayList<ResultFlag>();
      if ("id".equals(resultChild.getName())) {
        flags.add(ResultFlag.ID);
      }
      resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
    }
  }
  // ResultMapResolver的作用是生成ResultMap对象,并将其加入到Configuration对象的resultMaps容器中(具体过程见下)
  ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
  try {
    return resultMapResolver.resolve();
  } catch (IncompleteElementException  e) {
    configuration.addIncompleteResultMap(resultMapResolver);
    throw e;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

ResultMapResolver这个类很纯粹,有且仅有一个函数resolve,用于构造ResultMap对象,并将其存入Configuration对象的resultMaps容器中;而这个过程是借助于MapperBuilderAssistant.addResultMap完成的。

public ResultMap resolve() {
  return assistant.addResultMap(this.id, this.type, this.extend,  this.discriminator, this.resultMappings, this.autoMapping);
}
1
2
3
  • sqlElement函数

    该函数用于解析映射文件中所有的<sql>节点,并将这些节点存储在当前映射文件所对应的XMLMapperBuilder对象的sqlFragments容器中,供解析sql语句时使用。

<sql id="userColumns"> ${alias}.id,${alias}.username,${alias}.password </sql>
1
  • buildStatementFromContext函数 该函数会将映射文件中的sql语句解析成MappedStatement对象,并存在configurationmappedStatements
Last Updated: 10/20/2019, 11:49:45 PM